#include "filesys/bufcache.h"
#include "threads/malloc.h"
#include "devices/disk.h"
#include "threads/synch.h"
#include "devices/timer.h"
#include <stdbool.h>
#include <string.h>

#define ACCESSED 0x01
#define DIRTY 0x02
#define ALLOCATED 0x04

struct bufcache_entry
{
	/* int device_id; */
	disk_sector_t sector_idx;

	unsigned flag;
	struct lock entr_lock;
	uint8_t sec[DISK_SECTOR_SIZE]; /* sector data */
};

struct bufcache_entry *bufcache;
int next_entry_idx;

static struct lock bufwrite_lock;

struct bufcache_entry *find_empty_entry (struct disk *d, bool in_use);
struct bufcache_entry *buf_lookup (disk_sector_t sector_idx, bool in_use);
struct bufcache_entry *evict_entry (struct disk *d);
void clear_entry (struct bufcache_entry *e);

bool is_accessed(struct bufcache_entry* e);
bool is_dirty(struct bufcache_entry* e);
bool is_allocated(struct bufcache_entry* e);


inline bool is_accessed(struct bufcache_entry* e)
{
	return e->flag & ACCESSED;
}
inline bool is_dirty(struct bufcache_entry* e)
{
	return e->flag & DIRTY;
}
inline bool is_allocated(struct bufcache_entry* e)
{
	return e->flag & ALLOCATED;
}

void bufcache_init (void)
{
	bufcache = malloc (sizeof (struct bufcache_entry) * BUFFER_SIZE);
	ASSERT (bufcache != NULL);

	size_t i;
	for (i = 0; i<BUFFER_SIZE; i++)
	{
		bufcache[i].flag = 0;
		lock_init(&bufcache[i].entr_lock);
	}
	next_entry_idx = 0;
	lock_init(&bufwrite_lock);
}

struct bufcache_entry *buf_lookup (disk_sector_t sector_idx, bool in_use)
{
	size_t i;
	for (i = 0; i<BUFFER_SIZE; i++)
	{
		struct bufcache_entry *e = (bufcache + i);
		lock_acquire(&e->entr_lock);
		if (is_allocated(e) && e->sector_idx == sector_idx)
		{
			if(!in_use)
				lock_release(&e->entr_lock);
			return (bufcache + i);
		}
		lock_release(&e->entr_lock);
	}

	return NULL;
}

void block_read (struct disk *d, disk_sector_t sec_no, void *buffer)
{
	byte_read (d, sec_no, 0, DISK_SECTOR_SIZE, buffer);
}

void byte_read (struct disk *d, disk_sector_t sec_no, int sector_ofs, int chunk_size, void *buffer)
{
	bool is_new = false;
	struct bufcache_entry *e = buf_lookup (sec_no, true);

	if (e == NULL)
	{
		e = find_empty_entry (d, true);

		/* Fetch the block from the disk into cache. */
		disk_read (d, sec_no, e->sec);
		e->sector_idx = sec_no;
		e->flag |= ALLOCATED;
		is_new = true;
	}
	ASSERT (e->sec != NULL);
	memcpy (buffer, e->sec + sector_ofs, chunk_size);

	e->flag |= ACCESSED;
	lock_release(&e->entr_lock);
	if(!is_new)
		return;
	struct bufcache_entry *ahead = buf_lookup(sec_no+1, false);
	if(ahead == NULL)
	{
		ahead = find_empty_entry(d, true);
		disk_read (d, sec_no +1, ahead->sec);
		ahead->sector_idx = sec_no+1;
		ahead->flag |= ALLOCATED;
		ahead->flag |= ACCESSED;
		lock_release(&ahead->entr_lock);
	}
}

void block_write (struct disk *d, disk_sector_t sec_no, void *buffer)
{
	byte_write (d, sec_no, 0, DISK_SECTOR_SIZE, buffer);
}

void byte_write (struct disk *d, disk_sector_t sec_no, int sector_ofs, int chunk_size, void *buffer)
{
	struct bufcache_entry *e = buf_lookup (sec_no, true);

	if (e == NULL)
	{
		e = find_empty_entry (d, true);

		/* Fetch the block from the disk into cache. */
		disk_read (d, sec_no, e->sec);
		e->sector_idx = sec_no;
		e->flag |= ALLOCATED;
	}

	ASSERT (e->sec != NULL);
	memcpy (e->sec + sector_ofs, buffer, chunk_size);

	e->flag |= ACCESSED;
	e->flag |= DIRTY;
	lock_release(&e->entr_lock);
}

struct bufcache_entry *find_empty_entry (struct disk *d, bool in_use)
{
	struct bufcache_entry *e = NULL;

	size_t i;
	for (i=0; i<BUFFER_SIZE; i++)
	{
		e = (bufcache + i);
		lock_acquire(&e->entr_lock);
		if (!is_allocated(e))
		{
			if(!in_use)
				lock_release(&e->entr_lock);
			return e;
		}
		lock_release(&e->entr_lock);
	}

	e = evict_entry (d);

	if(!in_use)
			lock_release(&e->entr_lock);

	return e;
}

struct bufcache_entry *evict_entry (struct disk *d)
{
	struct bufcache_entry *e = NULL;
	while (true)
	{
		e = &bufcache[next_entry_idx];
		lock_acquire(&e->entr_lock);
		if (is_allocated(e))
		{
			if (!is_accessed(e))
				break;
			else
				e->flag &= ~ACCESSED;
		}
		lock_release(&e->entr_lock);
		next_entry_idx = (next_entry_idx + 1) % BUFFER_SIZE;
	}

	next_entry_idx = (next_entry_idx + 1) % BUFFER_SIZE;
		/* Evict an older entry. */
	if (is_dirty(e))
	{
		disk_write (d, e->sector_idx, e->sec);
	}
	clear_entry (e);
	return e;
}

void clear_entry (struct bufcache_entry *e)
{
	ASSERT (e != NULL);

	e->sector_idx = 0;
	e->flag = 0;
}

void flush_dirty_blocks (struct disk *disk)
{
	struct bufcache_entry *e;
	size_t i;
	lock_acquire(&bufwrite_lock);
	for (i = 0; i < BUFFER_SIZE; i++)
	{
		e = &bufcache[i];

		if (is_dirty(e))
		{
			disk_write (disk, e->sector_idx, e->sec);
			e->flag &= ~DIRTY;
		}
	}
	lock_release(&bufwrite_lock);
}

/* Thread function of the thread 
	 that flushes dirty blocks periodically. */
void thread_flush_dirty_blocks (void *disk_)
{
	struct disk *disk = disk_;

	for (;;)
	{
		timer_sleep (100);
		flush_dirty_blocks (disk);
	}
}
